#!/usr/bin/python

"""
A devices is a collection of cells to which structures can be mapped.
"""

import csv
import random

from utils import *
import system
import mapper

from low_level import Cellfield
from logger import logger, mapping_logger

class DeviceError(Exception):
    """
    handling class for adder exceptions
    """
    
    def __init__(self, msg):
        """
        initialize the class
        """
        logger.error("device: %s" % msg)

class Device(Cellfield):
    """
    Defines the device to which all of the structures are mapped
    """
    
    inputs = []
    
    def __init__(self, dimension = DEVICE_DIMENSION, proportion = 1, type = 'O'):
        """
        initialize the device
        """
        self.system_list= []
        self.fault_list = []
        Cellfield.__init__(self, dimension, proportion, type)
    
    def print_types(self):
        """
        print the types of the cells in the device
        """
        for row in self.cellfield:
            types = ''
            for entry in row:
                types += entry.get_type() + ' '
            print types

    def getwidth(self):
        return self.width
        
    def getheight(self):
        return self.height
        
    def add_faults(self, fault_map, num_faults):
        """
        map and inject hard faults into the device

        @param fault_map: the name of a file listing faults to be mapped
        @param num_faults: the number of additional faults to randomly inject

        @note: for now, a fault renders the entire cell useless
        """

        #store the number of faults found in the fault map file
        num_mapped_faults = len(self.fault_list)

        if fault_map != '':
            #clear the fault list
            self.fault_list = []
            #read the fault map file
            f = open(fault_map, 'r')
            lines = csv.reader(f)
            for line in lines:
                self.fault_list.append(line)

        for x, y in self.fault_list:
            self.cellfield[int(y)][int(x)].set_type('F')

        #there can't be a negative number of faulty cells
        if num_faults < 0:
            logger.warning('negative number of faults. Injecting zero faults.')
            num_faults = 0

        #at most, every device cell can be faulty
        num_cells = self.width * self.height
        if num_faults > num_cells - num_mapped_faults:
            logger.warning('more faults than cells. Injecting %d faults.' % num_cells)
            num_faults = num_cells - num_mapped_faults

        for iteration in range(num_faults):
            cell_type = 'F'

            #find a functional cell
            while cell_type == 'F':
                fault_x = random.randint(0, self.width - 1)
                fault_y = random.randint(0, self.height - 1)

                cell_type = self.cellfield[fault_y][fault_x].get_type()

            #inject the fault
            self.cellfield[fault_y][fault_x].set_type('F')
            #add the coordinates of the fault to the fault map
            self.fault_list.append((fault_x, fault_y))

    def add_system(self, filename):
        """
        add a system to the device

        @param filename: the name of a file describing the system to add
        """
        self.system_list.append(system.System(filename))

    def map(self, map_alg):
        """
        map all of the structures of all of the systems to the device

        @param map_alg: a text string indicating which mapping algorithm to use
        """

        #flag to keep track of success or failure of mapping a system
        success = False

        #flags used by the mapping algorithm
        avoid_faults = False
        order_by_size = False
        rotate = False

        #default to the simple fault avoidance algorithm
        if map_alg == 'default':
            map_alg = 'simple'

        if map_alg not in ["naive", "simple", "sizeaware", "sizeaware-rotate"]:
            raise DeviceError('mapping algorithm not recognized')

        if map_alg in ["simple", "sizeaware", "sizeaware-rotate"]:
            avoid_faults = True

        if map_alg in ["sizeaware", "sizeaware-rotate"]:
            order_by_size = True

        if map_alg in ["sizeaware-rotate"]:
            rotate = True

        for system in self.system_list:
            (self.cellfield, success) = mapper.map(self.cellfield, system, avoid_faults, order_by_size, rotate)
            if success:
                #log success
                mapping_logger.info('%s, 1, %d' % (map_alg, len(self.fault_list)))
            else:
                #log failure
                raise DeviceError('mapping failed')
                mapping_logger.info('%s, 0, %d' % (map_alg, len(self.fault_list)))

    def run(self, input_file, output_file):
        """
        process the inputs using the systems mapped to the device
        read the inputs from a file, write the outputs to a file

        @param input_file: the name of the file to read inputs from
        @param output_file: the name of the file to write outputs to
        """
        self.inputs= []
        self.read_inputs(input_file)
        #self.adjust_inputs()
        self.outputs= []
        self.execute()
        self.write_outputs(output_file)

    def execute(self):
        """
        fill the system pipelines and execute the inputs
        """
        #the first line of the inputs dictates the order of the columns
        input_order = self.inputs[0]
        #fill the pipe with the other inputs
        for input in self.inputs[1:]:
            for system in self.system_list:
                #add the input to the roots
                system.pipe_push(input, input_order)
                #advance the pipe
                results = system.pipe_step()
                self.outputs.append(results)
        #empty the pipe (keep stepping until the delay is compensated)
        for system in self.system_list:
            for delay_step in range(1, system.size()):
                results = system.pipe_step()
                self.outputs.append(results)
            #compensate for the length of the pipe (-1 because zero-based)
            self.outputs = self.outputs[system.size()-1:]
            #prepend a list of the leaf nodes
            self.outputs = [system.get_leaf_list()] + self.outputs
            
    def read_inputs(self, filename):
        """
        open the input file and read the inputs

        @param filename: the name of the file to read inputs from
        """
        f = open(filename, "r")
        lines = csv.reader(f)
        for line in lines:
            self.inputs.append(line)
        f.close()
        
    def adjust_inputs(self):
        """
        Format the inputs so they are ready for processing
        
        Ensure that each input has the same number of bits
        """
        longest = 0
        adjusted_inputs = []
        adjusted_entry = []
        
        #find the length of the longest input
        for line in self.inputs[1:]:
            for entry in line:
                if len(entry) > longest:
                    longest = len(entry)
        
        #pad the others accordingly
        adjusted_inputs.append(self.inputs[0])
        for line in self.inputs[1:]:
            adjusted_entry = []
            for entry in line:
                #pad with leading zeros
                adjusted_entry.append('0' * (longest - len(entry)) + entry)
            adjusted_inputs.append(adjusted_entry)
                
        self.inputs = adjusted_inputs
        
    def write_outputs(self, filename):
        """
        write the outputs to a file

        @param filename: the name of the file to write outputs to
        """
        
        f = open(filename, 'w')
        writer = csv.writer(f)
        writer.writerows(self.outputs)
        f.close()
        
        #get a list of the celltypes
        cellfield_types = []
        for row in self.cellfield:
            type_row = []
            for entry in row:
                type_row.append(entry.get_type())
            cellfield_types.append(type_row)
        #write this list to file
        f = open(".device", 'w')
        writer = csv.writer(f)
        writer.writerows(cellfield_types)
        f.close()

    def write_faults(self, filename):
        """
        write the fault map to a file

        @param filename: the name of the file to write the list of faults to
        """
        
        f = open(filename, 'w')
        writer = csv.writer(f)
        writer.writerows(self.fault_list)
        f.close()
